# -*- coding: utf-8 -*-
from tornado.web import UIModule

from bopress import options
from bopress.hook import Hooks
from bopress.taxonomy import create_unnamed_taxonomy, get_unnamed_term_taxonomy_id, get_term_taxonomys, \
    create_default_taxonomy
from bopress.utils import Utils

__author__ = 'yezang'


class Screen(object):
    def __init__(self, screen_id="", handler=None):
        self.id = screen_id
        self.handler = handler
        self.current_menu = None


class AbstractListTableDataProvider(object):
    """
        For BoListTable DataSource And Config.
    """

    def __init__(self):
        pass

    def require_auth(self):
        """
        Whether require user auth.

        :return: bool
        """
        return True

    def primary_key(self, handler):
        """
        Entity model primary key, single key.

        :param handler: `.handlers.BaseHandler`
        :return: str
        """
        return ""

    def dropdown_actions(self, handler):
        """

        dropdown bulk actions

        :param handler: `.handlers.BaseHandler`
        :return: list or tuple

            example::
                return [("approve", "Approve the project",
                                {"before_bulk_actions":"func1", "after_bulk_actions":"func2"}), ..]

            The third value is dict for javascript hook. optional.
            ``before_bulk_actions``, ``after_bulk_actions``, can hook one or all.

            in your template javascript code look like this::
                // primary_keys is array.
                function func1(params, api_url){
                    // do your action here.
                    // return false for stop ajax request.
                    return true;
                }
                // rsp is ajax response result.
                function func2(rsp, params, api_url){
                    return true;
                }

        """
        return []

    def row_actions(self, handler):
        """

        data row actions

        :param handler: `.handlers.BaseHandler`
        :return: dict, key is data column for render action menu.

        example::
                return {"post_title":[("approve", "Approve the project",
                                {"before_row_action":"func1", "after_row_action":"func2"}), ..]}

        dict value:
        The third value is dict for javascript hook. optional.
        ``before_row_action``, ``after_row_action``, can hook one or all.

        in your template javascript code look like this::

                // primary_keys is array.
                function func1(action_name, primary_keys, api_url){
                    // do your action here.
                    // return false for stop ajax request.
                }
                // rsp is ajax response result.
                function func2(rsp, params, api_url){

                }
        """
        return dict()

    def column_titles(self, handler):
        """
        jquery dataTables column titles, return list or tulpe

        example::
            return ['Title 1', 'Title 2', ..]

        :param handler: `.handlers.BaseHandler`
        """
        raise NotImplementedError()

    def columns(self, handler):
        """
        jquery dataTables field name from api result, return list or tulpe::

            return ['post_id', 'title', 'content', ..]

        :param handler: `.handlers.BaseHandler`
        """
        raise NotImplementedError()

    def column_render_cb(self):
        """
        javascript function name for jquery dataTables column render callback.

            example, define javascript function ``col_render`` in your template::

                function col_render(field_name, data, type, row){
                    if(field_name=="user_status"){
                        if(data==1){
                            return '激活';
                        }
                        return '未激活';
                    }
                    return data;
                }

        :return: str
        """
        return ""

    def ajax_params_cb(self):
        """

        javascript function name for jquery dataTables ajax request extra params.

            example, define javascript function ``extra_params`` in your template::

                def extra_params(params){
                    params.custom = $('#myInput').val();
                }

        :return: str
        """
        return ""

    def data(self, handler, params):
        """

        Dataset for jquery dataTables to load.
        full example, please refer users manage in ``coreplugins.py``

        :param dict params: jquery datatables query params.
        :param handler: `.handlers.BaseHandler`

            example::
                s = SessionFactory.session()
                q = s.query(Posts)
                q = self.paginate(q, params["start"], params["length"])
                # can use ``params`` to build dynamic filter.
                items = q.all()
                count = s.query(Posts).count()
                self.render_json(handler, dataset=items, records_total=count, records_filtered=count, params=params)

        """
        raise NotImplementedError()

    def do_actions(self, action_name, handler):
        """

        Deal with dropdown actions or data row action.

        :param action_name: action name, uniq.
        :param handler: `.handlers.BaseHandler`

            example::

                arr = handler.get_arguments('primary_keys[]')
                if action_name == 'delete':
                    pass

        """
        pass

    def do_data_save(self, handler):
        """

        Deal with form data save.
        Notice, submit data for create or update, can test primary key is none or not none.
        if success, must return current saved object primary key and it value. Example::
            handler.render_json({'user_id': 'val'}, '200', True)


        :param handler: `.handlers.BaseHandler`

            use wtforms::
                s = SessionFactory.session()
                m = SomeModel()
                f = SomeForm(`.forms.FormData`(handler))
                f.populate_obj(m)
                s.add(m)
                s.commit()
                ..
        """
        pass

    def searchable_columns(self, handler):
        """

        Searchable data columns

        :param handler: `.handlers.BaseHandler`
        :return: list or tuple

            example::
                return ['title', 'content']

        """
        return []

    def orderable_columns(self, handler):
        """
        Orderable data columns

        :param handler: `.handlers.BaseHandler`
        :return: list or tuple

            example::
                return ['title', ..]

        """
        return []

    def hide_columns(self, handler):
        """

        Hide data columns

        :param handler: `.handlers.BaseHandler`
        :return: list or tuple

            example::
                return ['post_id', ..]

        """
        return []

    def paginate(self, q, start=0, limit=10):
        """

        Helper function for paginate.

        :param q: `sqlalchemy.orm.query.Query` object.
        :param start: offset records num.
        :param limit: how much records to display.
        :return: `sqlalchemy.orm.query.Query` object.
        """
        return q.offset(start).limit(limit)

    def first_order_column(self, params=None):
        if not params:
            return None
        orders = params['order']
        if len(orders) > 0:
            item = orders[0]
            col_index = item['column']
            order_dir = item['dir']
            col_name = params['columns'][col_index]['data']
            return col_name, order_dir
        return None

    def render_json(self, handler, dataset=None, records_total=0, records_filtered=0, params=None):
        """
        Render json data for jquery dataTables to load.

        :param handler: `.handlers.BaseHandler`
        :param params: jquery dataTables query params.
        :param dataset: dataset for display
        :param records_total: total records.
        :param records_filtered: total filtered records.
        """
        if not dataset:
            dataset = list()
        r = dict()
        r["draw"] = params.get("draw", 1)
        r["recordsTotal"] = records_total
        r["recordsFiltered"] = records_filtered
        r["data"] = dataset
        handler.head_json()
        handler.write(Utils.encode(r))

    def forms(self, handler):
        """
        Wtforms instances.
        :param handler: `.handlers.BaseHandler`
        :return: list or tuple
        """
        return []

    def form_titles(self, handler):
        """

        Form title, use for panel title or tab title.

        :param handler: `.handlers.BaseHandler`
        :return: list or tuple
        """
        return []

    def form_layouts(self, handler):
        """

        for `BoForm`s wtforms fields layouts.

        layouts example::

            {
                0: ( ('col1', 'col2', (4,8)), '_div_id', ('col3'), ('col4','col5','col6') ),
                ...
            }

        ``0`` is form index.
        ``(4,8)`` bootstrap 12 column grid layout.
        ``_div_id`` if startswith ``_`` will be render to ``div`` tag, otherwise render to html headline tag.

        :param handler: `.handlers.BaseHandler`
        :return: dict
        """
        return dict()

    def forms_footer(self, handler):
        """

        Add your html code inside the form. such as hide inputs or div or other html elements.

        :param handler: `.handlers.BaseHandler`
        :return: str
        """
        return ""

    def before_from_data_save_cb(self, handler):
        """

        Before form data save, Javascript function name for callback.

        :param handler: `.handlers.BaseHandler`
        :return: str

        example::
                // flag is save_and_exit, save_and_edit, save_and_new
                function cb(flag){

                }

        """
        return ""

    def after_from_data_save_cb(self, handler):
        """
        After form data save, Javascript function name for callback.
        :param handler: `.handlers.BaseHandler`
        :return: str

            example::
                // flag is save_and_exit, save_and_edit, save_and_new
                function cb(flag, response){

                }
        """
        return ""

    def show_add_button(self, handler):
        """
        Whether display add button.
        :param handler: `.handlers.BaseHandler`
        :return: bool
        """
        return True

    def show_save_and_edit_button(self, handler):
        """
        Whether display save and edit button.
        :param handler: `.handlers.BaseHandler`
        :return: bool
        """
        return True

    def show_save_and_new_button(self, handler):
        """
        Whether display save and add button.
        :param handler: `.handlers.BaseHandler`
        :return: bool
        """
        return True

    def show_save_and_exit_button(self, handler):
        """
        Whether display save and exit button.
        :param handler: `.handlers.BaseHandler`
        :return: bool
        """
        return True


class BoForm(UIModule):
    @staticmethod
    def _create_form(form, layout):
        """
        表单创建
        :param form: `WTForm`
        :param layout: 表单布局或者 ``None``
        :return: str
        """
        html = ''
        if layout:
            for cols in layout:
                cols_type = type(cols)
                if cols_type is str:
                    # div
                    if cols.startswith("_"):
                        html += '<div class="row"><div id="{0}" class="col-xs-12"></div></div>'.format(cols)
                    # h3
                    else:
                        html += '<div class="row"><div class="col-xs-12"><h4>{0}</h4></div></div>'.format(cols)
                else:
                    # form controls
                    size = len(cols)
                    if size > 0:
                        row_rule = cols[-1]
                        row_rule_datatype = type(row_rule)
                        if row_rule_datatype is str:
                            row_rules = list()
                            col_space = int(12 / size)
                            for i in range(size):
                                row_rules.append(col_space)
                        else:
                            row_rules = row_rule
                        index = 0
                        html += '<div class="row">'
                        for c in cols:
                            # 正常项为数据字段名称，结尾可以是一个元组或者数组来表示行空间分配
                            if type(c) is not str:
                                break
                            field = form[c]
                            html += '<div class="col-xs-{0}">'.format(row_rules[index])
                            html += '<div class="form-group">'
                            html += str(field.label)
                            html += str(field(class_="form-control"))
                            html += '</div>'
                            html += '</div>'
                            index += 1
                        html += '</div>'
        else:
            # form controls, one column
            html += '<div class="row">'
            for field in form:
                html += '<div class="col-xs-12">'
                html += '<div class="form-group">'
                html += str(field.label)
                html += str(field(class_="form-control"))
                html += '</div>'
                html += '</div>'
            html += '</div>'
        return html

    def render(self, forms, form_titles=list(), layouts=None):
        """
        渲染Widget
        :param form_titles: tab title, list or tuple
        :param forms: wtform, list<wtform()> or tuple<wtform()>
        :param dict layouts: key is `forms` index.
            如果是``'_div_id'``以``_``开头会渲染成一个div层，否则会当做表单内标题
            ``(4,8)``以12列为基准进行行空间分配，可选，如果没有指定则自动平均分配
            layouts::
                {
                    0: (('col1', 'col2', (4,8)),'_div_id',('col3'),('col4','col5','col6'))
                }
        """
        if not forms:
            return ""
        if not layouts:
            layouts = dict()
        html = ""
        if len(forms) > 1:
            tabs = list()
            contents = list()
            index = 0
            for title in form_titles:
                if index == 0:
                    li_elm = """
                    <li class="active"><a href="#bs_tab_{0}" data-toggle="tab" aria-expanded="false">{1}</a></li>
                    """
                    tabs.append(li_elm.format(index, title))
                    contents.append('<div class="tab-pane active" id="bs_tab_%i">{%s}</div>' % (index, index))
                else:
                    tabs.append('<li><a href="#bs_tab_{0}" data-toggle="tab" aria-expanded="false">{1}</a></li>'
                                .format(index, title))
                    contents.append('<div class="tab-pane" id="bs_tab_%i">{%s}</div>' % (index, index))
                index += 1
            html += '<div class="nav-tabs-custom">'
            html += '<ul class="nav nav-tabs">{0}</ul><div class="tab-content">{1}</div>' \
                .format("".join(tabs), "".join(contents))
            html += "</div>"
            tab_forms = list()
            i = 0
            for f in forms:
                layout = layouts.get(i)
                tab_forms.append(BoForm._create_form(f, layout))
                i += 1
            html = html.format(*tab_forms)
        else:
            html = BoForm._create_form(forms[0], layouts.get(0))
        return html


class BoListTable(UIModule):
    def render(self, table_id="", data_provider_cls=None):
        """

        :param table_id: html elem id, jquery dataTables id.
        :param data_provider_cls:
        :return:
        """
        if not table_id and not data_provider_cls:
            return ""
        self.api_name = Utils.md5(table_id)
        tables = Hooks.ui_list_tables()
        provider = tables.get(self.api_name, None)
        if not provider:
            provider = data_provider_cls()
            tables[self.api_name] = provider
        self.table_id = table_id
        return self.render_string("bocore/admin/ui/listtable_body.html",
                                  table_id=table_id,
                                  provider=provider)

    def html_body(self):
        return self.render_string("bocore/admin/ui/listtable_script.html",
                                  table_id=self.table_id,
                                  provider=Hooks.ui_list_tables()[self.api_name],
                                  api_name=self.api_name
                                  )

    def javascript_files(self):
        return [
            "theme/plugins/datatables/jquery.dataTables.min.js",
            "theme/plugins/datatables/dataTables.bootstrap.min.js",
            "theme/plugins/select2/select2.full.min.js",
            "js/bo-listtable-helper.js"
        ]

    def css_files(self):
        return [
            "theme/plugins/datatables/dataTables.bootstrap.css",
            "theme/plugins/select2/select2.min.css"
        ]


class BoTree(UIModule):
    def render(self, elem_id, taxonomy, title='', caps=()):
        if not elem_id or not taxonomy:
            return ""
        if not title:
            title = taxonomy
        self.elem_id = elem_id
        self.taxonomy = taxonomy
        self.title = title
        unnamed_term_taxonomy_id = get_unnamed_term_taxonomy_id(taxonomy)
        if not unnamed_term_taxonomy_id:
            create_unnamed_taxonomy(taxonomy)
        if not caps:
            tt_key = "bo_termtaxonomy_{0}".format(taxonomy)
            options.save_options(tt_key, set(caps))
        return self.render_string("bocore/admin/ui/ztree_body.html",
                                  elem_id=elem_id,
                                  taxonomy=taxonomy, title=title
                                  )

    def html_body(self):
        if not self.elem_id or not self.taxonomy:
            return ""
        node_set = get_term_taxonomys(self.taxonomy)
        nodes = list()
        node_root = dict()
        node_root['id'] = ''
        node_root['pId'] = ''
        node_root['name'] = self.title
        node_root['open'] = True
        nodes.append(node_root)
        for d in node_set:
            node = dict()
            node['id'] = d[0]
            node['pId'] = d[1]
            node['name'] = d[2]
            nodes.append(node)
        return self.render_string("bocore/admin/ui/ztree_script.html",
                                  elem_id=self.elem_id,
                                  taxonomy=self.taxonomy,
                                  nodes=nodes, title=self.title)

    def javascript_files(self):
        return [
            "js/ztree/js/jquery.ztree.core.min.js",
            "js/ztree/js/jquery.ztree.excheck.min.js",
            "js/ztree/js/jquery.ztree.exedit.min.js",
            "js/parsley/parsley.min.js",
            "js/parsley/i18n/zh_cn.js",
            "js/bo-ztree-helper.js"
        ]

    def css_files(self):
        return [
            "js/ztree/css/awesomeStyle/awesome.css"
        ]


class BoUploader(UIModule):
    def render(self, elem_id, tt_name="Attachment", tt_slug="attachment", max_file_size=10,
               uploaded_javascript_callback="", file_extensions="jpg,gif,png,zip,pdf"):
        """
        Render plupload.

        :param elem_id: html elem id and as taxonomy
        :param tt_name: term taxonomy name
        :param tt_slug: term taxonomy slug
        :param max_file_size: file upload limit size
        :param uploaded_javascript_callback: client js callback
        :param file_extensions: file ext, ``jpg,gif,png,zip,pdf``.. etc.
        :return: str

            Demo::

                def file_upload(paths, name):
                    if name == "report":
                        # ``filename`` no need ext.
                        # upload folder and relative path and use origin uploadfile name.
                        # relative path can be empty, will be auto use uuid.
                        # if relative path empty: `True` use origin file name, `False` use uuid with date style path.
                        return '/monnt/upload', '2017/01/01/filename', True
                    return paths

                add_filter('bo_fileupload', file_upload)

            In your template::
                {% module BoUploader('report', 'ReportFile', 'report_file',
                            uploaded_javascript_callback='report_fileuploaded') %}

            Uploaded Js callback::
                function report_fileuploaded(rsp, file) {
                    // ``rsp`` contain attachment_id and relative path.
                    // ``file`` is plupload file param.
                    console.log(rsp, file);
                }
        """
        tt = create_default_taxonomy(tt_name, tt_slug, elem_id)
        self.term_taxonomy_id = tt.term_taxonomy_id
        self.elem_id = elem_id
        self.max_file_size = max_file_size
        self.uploaded_javascript_callback = uploaded_javascript_callback
        self.file_extensions = file_extensions
        return self.render_string("bocore/admin/ui/upload.html", elem_id=elem_id,
                                  term_taxonomy_id=self.term_taxonomy_id)

    def javascript_files(self):
        return [
            "js/plupload/plupload.full.min.js"
        ]

    def html_body(self):
        return self.render_string("bocore/admin/ui/upload_script.html", elem_id=self.elem_id,
                                  max_file_size=self.max_file_size,
                                  uploaded_javascript_callback=self.uploaded_javascript_callback,
                                  file_extensions=self.file_extensions, term_taxonomy_id=self.term_taxonomy_id)
